Primary Goal
Your task is to identify the photographer of the 20 test images using two distinct approaches.
In this lab, you will explore whether you can differentiate photos taken by different people. Specifically, you’ll work with pictures taken by Dr. Alex Dekhtyar, Dr. Kelly Bodwin, or your instructor. You’ll use two machine learning approaches to determine the likely photographer for 20 unknown test images.
The dataset contains:
Note: These images are not formatted like MNIST, so you’ll need to research how to import and process them in Python.
Your task is to identify the photographer of the 20 test images using two distinct approaches.
Train individual (convolutional) neural networks for the following image features:
Develop at least 10 additional features (or feature categories) that may help distinguish between Alex’s and Kelly’s photos. For each, train a separate (convolutional) neural network.
Treat the outputs of all 14+ feature models as inputs to a final classification model that predicts the photographer (Alex or Kelly).
The example code below was developed with guidance and inspiration from the following resources:
ImageDataGeneratorimage_dataset_from_directoryimport os
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models, callbacks, applications, regularizers
from sklearn.utils import class_weight
# Set Constant Image Size
IMG_HEIGHT = 128
IMG_WIDTH = 128
BATCH_SIZE = 32
SEED = 42
import random
def set_global_seed(seed=SEED):
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)# Paths
train_dir = "Alex_Kelly_Pics/train"
test_dir = "Alex_Kelly_Pics/test"
# Load training and validation datasets
train_ds = tf.keras.utils.image_dataset_from_directory(
train_dir,
validation_split=0.2,
subset="training",
seed=SEED,
image_size=(IMG_HEIGHT, IMG_WIDTH),
batch_size=BATCH_SIZE,
label_mode="categorical"
)
val_ds = tf.keras.utils.image_dataset_from_directory(
train_dir,
validation_split=0.2,
subset="validation",
seed=SEED,
image_size=(IMG_HEIGHT, IMG_WIDTH),
batch_size=BATCH_SIZE,
label_mode="categorical"
)
# Load test dataset (unlabeled)
test_ds = tf.keras.utils.image_dataset_from_directory(
test_dir,
labels=None,
shuffle=False,
image_size=(IMG_HEIGHT, IMG_WIDTH),
batch_size=1
)Found 715 files belonging to 3 classes.
Using 572 files for training.
Found 715 files belonging to 3 classes.
Using 143 files for validation.
Found 20 files.
def visualize_batch(dataset, class_names):
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(10, 6))
for images, labels in dataset.take(1):
for i in range(8):
ax = plt.subplot(2, 4, i + 1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[np.argmax(labels[i].numpy())])
plt.axis("off")
plt.tight_layout()
plt.show()
visualize_batch(train_ds, class_names=["Alex", "Kelly", "Neither"])To prepare our images for training, we normalize the pixel values using tf.keras.layers.Rescaling(1./255). This layer scales the original RGB values from the range [0, 255] down to [0, 1], which is a standard practice in deep learning. Normalization helps the model train more efficiently by stabilizing gradients, improving convergence, and ensuring that inputs are on a consistent scale, especially important when using activation functions like ReLU or Sigmoid.
# Normalize images to [0, 1] range
normalization_layer = tf.keras.layers.Rescaling(1. / 255)
train_ds = train_ds.map(lambda x, y: (normalization_layer(x), y)).cache().prefetch(buffer_size=tf.data.AUTOTUNE)
val_ds = val_ds.map(lambda x, y: (normalization_layer(x), y)).cache().prefetch(buffer_size=tf.data.AUTOTUNE)
test_ds = test_ds.map(lambda x: normalization_layer(x)).cache().prefetch(buffer_size=tf.data.AUTOTUNE)def train_feature_cnn(feature_dir, img_size=(IMG_HEIGHT, IMG_WIDTH), batch_size=32, epochs=20):
train_path = os.path.join(feature_dir, "train")
# Load unbatched dataset to extract class distribution
raw_train_ds = tf.keras.utils.image_dataset_from_directory(
train_path,
image_size=img_size,
batch_size=None, # unbatched for label extraction
validation_split=0.2,
subset="training",
seed=42,
label_mode="binary"
)
# Extract labels to compute class weights
label_list = [int(y.numpy()) for _, y in raw_train_ds]
class_weights = class_weight.compute_class_weight(
class_weight='balanced',
classes=np.unique(label_list),
y=label_list
)
class_weights_dict = dict(enumerate(class_weights))
# Load batched training and validation datasets
train_ds = raw_train_ds.batch(batch_size)
val_ds = tf.keras.utils.image_dataset_from_directory(
train_path,
image_size=img_size,
batch_size=batch_size,
validation_split=0.2,
subset="validation",
seed=SEED,
label_mode="binary"
)
# Data augmentation for training set
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1),
layers.RandomZoom(0.1),
layers.RandomContrast(0.1),
])
rescale = layers.Rescaling(1./255)
train_ds = train_ds.map(lambda x, y: (data_augmentation(rescale(x)), y)).cache().prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.map(lambda x, y: (rescale(x), y)).cache().prefetch(tf.data.AUTOTUNE)
# CNN model
model = models.Sequential([
tf.keras.Input(shape=img_size + (3,)),
layers.Conv2D(32, (3, 3), activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(64, (3, 3), activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D(),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dropout(0.5),
layers.Dense(1, activation='sigmoid')
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
early_stop = callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
# Train with class weights
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=epochs,
callbacks=[early_stop],
class_weight=class_weights_dict
)
return model, historyFound 890 files belonging to 2 classes.
Using 712 files for training.
Found 890 files belonging to 2 classes. Using 178 files for validation. Epoch 1/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 4s 108ms/step - accuracy: 0.4764 - loss: 0.7500 - val_accuracy: 0.6124 - val_loss: 0.6867 Epoch 2/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 69ms/step - accuracy: 0.5126 - loss: 0.6816 - val_accuracy: 0.6180 - val_loss: 0.6734 Epoch 3/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 70ms/step - accuracy: 0.5880 - loss: 0.6597 - val_accuracy: 0.6236 - val_loss: 0.7147 Epoch 4/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 72ms/step - accuracy: 0.6295 - loss: 0.6302 - val_accuracy: 0.6348 - val_loss: 0.7257 Epoch 5/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 69ms/step - accuracy: 0.7079 - loss: 0.5897 - val_accuracy: 0.5787 - val_loss: 0.9422 Epoch 6/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.7462 - loss: 0.5428 - val_accuracy: 0.6236 - val_loss: 0.6471 Epoch 7/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.6886 - loss: 0.5834 - val_accuracy: 0.6348 - val_loss: 0.6592 Epoch 8/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 69ms/step - accuracy: 0.7414 - loss: 0.5341 - val_accuracy: 0.6573 - val_loss: 1.0375 Epoch 9/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.7906 - loss: 0.4180 - val_accuracy: 0.6404 - val_loss: 0.7624 Epoch 10/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.8405 - loss: 0.3499 - val_accuracy: 0.6742 - val_loss: 0.8931 Epoch 11/100 23/23 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.8831 - loss: 0.2789 - val_accuracy: 0.6798 - val_loss: 1.0346
Found 1424 files belonging to 2 classes.
Using 1140 files for training.
Found 1424 files belonging to 2 classes. Using 284 files for validation. Epoch 1/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 5s 101ms/step - accuracy: 0.5942 - loss: 0.7040 - val_accuracy: 0.8662 - val_loss: 0.3695 Epoch 2/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 69ms/step - accuracy: 0.8392 - loss: 0.4024 - val_accuracy: 0.8803 - val_loss: 0.3163 Epoch 3/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.8345 - loss: 0.3774 - val_accuracy: 0.8627 - val_loss: 0.3326 Epoch 4/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.8709 - loss: 0.3113 - val_accuracy: 0.8556 - val_loss: 0.3526 Epoch 5/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.8728 - loss: 0.2809 - val_accuracy: 0.8592 - val_loss: 0.3555 Epoch 6/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.8877 - loss: 0.2548 - val_accuracy: 0.8768 - val_loss: 0.3936 Epoch 7/100 36/36 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.8855 - loss: 0.2455 - val_accuracy: 0.8732 - val_loss: 0.3919
Found 770 files belonging to 2 classes.
Using 616 files for training.
Found 770 files belonging to 2 classes. Using 154 files for validation. Epoch 1/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 3s 103ms/step - accuracy: 0.5443 - loss: 0.7261 - val_accuracy: 0.7727 - val_loss: 0.5112 Epoch 2/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 67ms/step - accuracy: 0.7761 - loss: 0.5137 - val_accuracy: 0.7727 - val_loss: 0.4770 Epoch 3/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 68ms/step - accuracy: 0.7638 - loss: 0.4707 - val_accuracy: 0.7468 - val_loss: 0.4806 Epoch 4/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 68ms/step - accuracy: 0.8134 - loss: 0.4270 - val_accuracy: 0.7338 - val_loss: 0.5794 Epoch 5/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 70ms/step - accuracy: 0.8500 - loss: 0.3721 - val_accuracy: 0.7662 - val_loss: 0.5759 Epoch 6/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 70ms/step - accuracy: 0.8790 - loss: 0.3441 - val_accuracy: 0.7922 - val_loss: 0.5111 Epoch 7/20 20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 69ms/step - accuracy: 0.8664 - loss: 0.3207 - val_accuracy: 0.6623 - val_loss: 0.9277
444/444 ━━━━━━━━━━━━━━━━━━━━ 30s 67ms/step - accuracy: 0.9845 - loss: 0.0428 - val_accuracy: 0.9921 - val_loss: 0.0234 Epoch 8/20 444/444 ━━━━━━━━━━━━━━━━━━━━ 30s 66ms/step - accuracy: 0.9925 - loss: 0.0208 - val_accuracy: 0.9890 - val_loss: 0.0391 Epoch 9/20 444/444 ━━━━━━━━━━━━━━━━━━━━ 30s 67ms/step - accuracy: 0.9929 - loss: 0.0242 - val_accuracy: 0.9901 - val_loss: 0.0297 Epoch 10/20 444/444 ━━━━━━━━━━━━━━━━━━━━ 30s 67ms/step - accuracy: 0.9923 - loss: 0.0240 - val_accuracy: 0.9927 - val_loss: 0.0254
Found 1367 files belonging to 2 classes.
Using 1094 files for training.
Found 1367 files belonging to 2 classes. Using 273 files for validation. Epoch 1/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 6s 120ms/step - accuracy: 0.5404 - loss: 0.7430 - val_accuracy: 0.5531 - val_loss: 1.1912 Epoch 2/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.8130 - loss: 0.5208 - val_accuracy: 0.8974 - val_loss: 0.2875 Epoch 3/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 66ms/step - accuracy: 0.9113 - loss: 0.2514 - val_accuracy: 0.8938 - val_loss: 0.3370 Epoch 4/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 66ms/step - accuracy: 0.9287 - loss: 0.2010 - val_accuracy: 0.9121 - val_loss: 0.2000 Epoch 5/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 68ms/step - accuracy: 0.9315 - loss: 0.1572 - val_accuracy: 0.9158 - val_loss: 0.2380 Epoch 6/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 66ms/step - accuracy: 0.9549 - loss: 0.1181 - val_accuracy: 0.9231 - val_loss: 0.2204 Epoch 7/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 66ms/step - accuracy: 0.9612 - loss: 0.1225 - val_accuracy: 0.9231 - val_loss: 0.2037 Epoch 8/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.9712 - loss: 0.0832 - val_accuracy: 0.9377 - val_loss: 0.2100 Epoch 9/20 35/35 ━━━━━━━━━━━━━━━━━━━━ 2s 67ms/step - accuracy: 0.9780 - loss: 0.0672 - val_accuracy: 0.9341 - val_loss: 0.2276
Found 3997 files belonging to 2 classes.
Using 3198 files for training.
Found 3997 files belonging to 2 classes. Using 799 files for validation. Epoch 1/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 12s 111ms/step - accuracy: 0.7626 - loss: 0.4859 - val_accuracy: 0.9199 - val_loss: 0.2509 Epoch 2/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 92ms/step - accuracy: 0.9221 - loss: 0.2060 - val_accuracy: 0.9524 - val_loss: 0.1206 Epoch 3/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 93ms/step - accuracy: 0.9521 - loss: 0.1401 - val_accuracy: 0.9599 - val_loss: 0.1129 Epoch 4/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 10s 100ms/step - accuracy: 0.9531 - loss: 0.1308 - val_accuracy: 0.9612 - val_loss: 0.1002 Epoch 5/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 93ms/step - accuracy: 0.9693 - loss: 0.0959 - val_accuracy: 0.9587 - val_loss: 0.1238 Epoch 6/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 93ms/step - accuracy: 0.9774 - loss: 0.0787 - val_accuracy: 0.9549 - val_loss: 0.1448 Epoch 7/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 93ms/step - accuracy: 0.9850 - loss: 0.0626 - val_accuracy: 0.9349 - val_loss: 0.2072 Epoch 8/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 95ms/step - accuracy: 0.9864 - loss: 0.0478 - val_accuracy: 0.9374 - val_loss: 0.1988 Epoch 9/20 100/100 ━━━━━━━━━━━━━━━━━━━━ 9s 94ms/step - accuracy: 0.9829 - loss: 0.0462 - val_accuracy: 0.9574 - val_loss: 0.1491
Found 4198 files belonging to 2 classes.
Using 3359 files for training.
Found 4198 files belonging to 2 classes. Using 839 files for validation. Epoch 1/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 20s 167ms/step - accuracy: 0.7458 - loss: 0.5487 - val_accuracy: 0.9094 - val_loss: 0.2743 Epoch 2/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 100ms/step - accuracy: 0.9158 - loss: 0.2683 - val_accuracy: 0.9201 - val_loss: 0.2247 Epoch 3/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9128 - loss: 0.2363 - val_accuracy: 0.9297 - val_loss: 0.2114 Epoch 4/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 97ms/step - accuracy: 0.9198 - loss: 0.2138 - val_accuracy: 0.9285 - val_loss: 0.1978 Epoch 5/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 11s 104ms/step - accuracy: 0.9163 - loss: 0.2070 - val_accuracy: 0.9297 - val_loss: 0.2011 Epoch 6/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 12s 112ms/step - accuracy: 0.9363 - loss: 0.1839 - val_accuracy: 0.9261 - val_loss: 0.2132 Epoch 7/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 99ms/step - accuracy: 0.9448 - loss: 0.1516 - val_accuracy: 0.9201 - val_loss: 0.2331 Epoch 8/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 97ms/step - accuracy: 0.9502 - loss: 0.1258 - val_accuracy: 0.9213 - val_loss: 0.2099 Epoch 9/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 96ms/step - accuracy: 0.9573 - loss: 0.1156 - val_accuracy: 0.9309 - val_loss: 0.2064
Found 4198 files belonging to 2 classes.
Using 3359 files for training.
Found 4198 files belonging to 2 classes. Using 839 files for validation. Epoch 1/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 21s 183ms/step - accuracy: 0.7500 - loss: 0.5183 - val_accuracy: 0.8689 - val_loss: 0.3225 Epoch 2/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 96ms/step - accuracy: 0.8666 - loss: 0.3574 - val_accuracy: 0.8999 - val_loss: 0.2795 Epoch 3/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 96ms/step - accuracy: 0.8536 - loss: 0.3743 - val_accuracy: 0.8987 - val_loss: 0.2642 Epoch 4/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 94ms/step - accuracy: 0.8881 - loss: 0.3020 - val_accuracy: 0.8951 - val_loss: 0.2689 Epoch 5/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 95ms/step - accuracy: 0.8876 - loss: 0.2921 - val_accuracy: 0.9023 - val_loss: 0.2682 Epoch 6/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9022 - loss: 0.2668 - val_accuracy: 0.9023 - val_loss: 0.2587 Epoch 7/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 93ms/step - accuracy: 0.9154 - loss: 0.2211 - val_accuracy: 0.9106 - val_loss: 0.2567 Epoch 8/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9187 - loss: 0.2048 - val_accuracy: 0.9166 - val_loss: 0.2360 Epoch 9/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 9s 90ms/step - accuracy: 0.9260 - loss: 0.1889 - val_accuracy: 0.9106 - val_loss: 0.2448 Epoch 10/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 91ms/step - accuracy: 0.9329 - loss: 0.1645 - val_accuracy: 0.9142 - val_loss: 0.2460 Epoch 11/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 91ms/step - accuracy: 0.9542 - loss: 0.1318 - val_accuracy: 0.9213 - val_loss: 0.2414 Epoch 12/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 91ms/step - accuracy: 0.9427 - loss: 0.1299 - val_accuracy: 0.9094 - val_loss: 0.2500 Epoch 13/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 91ms/step - accuracy: 0.9611 - loss: 0.1099 - val_accuracy: 0.9118 - val_loss: 0.2868
Found 3823 files belonging to 2 classes.
Using 3059 files for training.
Found 3823 files belonging to 2 classes. Using 764 files for validation. Epoch 1/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 17s 160ms/step - accuracy: 0.7415 - loss: 0.5101 - val_accuracy: 0.7788 - val_loss: 0.5158 Epoch 2/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 90ms/step - accuracy: 0.8803 - loss: 0.3102 - val_accuracy: 0.8783 - val_loss: 0.3075 Epoch 3/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 89ms/step - accuracy: 0.8904 - loss: 0.2667 - val_accuracy: 0.8743 - val_loss: 0.2983 Epoch 4/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 92ms/step - accuracy: 0.9016 - loss: 0.2355 - val_accuracy: 0.9149 - val_loss: 0.2231 Epoch 5/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 94ms/step - accuracy: 0.9301 - loss: 0.1947 - val_accuracy: 0.8940 - val_loss: 0.2547 Epoch 6/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 94ms/step - accuracy: 0.9380 - loss: 0.1686 - val_accuracy: 0.8992 - val_loss: 0.3011 Epoch 7/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 95ms/step - accuracy: 0.9407 - loss: 0.1636 - val_accuracy: 0.9110 - val_loss: 0.2770 Epoch 8/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 98ms/step - accuracy: 0.9481 - loss: 0.1461 - val_accuracy: 0.9149 - val_loss: 0.2815 Epoch 9/20 96/96 ━━━━━━━━━━━━━━━━━━━━ 9s 97ms/step - accuracy: 0.9614 - loss: 0.1211 - val_accuracy: 0.9018 - val_loss: 0.3680
Found 4163 files belonging to 2 classes.
Using 3331 files for training.
Found 4163 files belonging to 2 classes. Using 832 files for validation. Epoch 1/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 19s 165ms/step - accuracy: 0.6628 - loss: 0.5792 - val_accuracy: 0.9075 - val_loss: 0.2265 Epoch 2/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 95ms/step - accuracy: 0.8931 - loss: 0.2931 - val_accuracy: 0.9087 - val_loss: 0.2364 Epoch 3/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9016 - loss: 0.2770 - val_accuracy: 0.9183 - val_loss: 0.1992 Epoch 4/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 94ms/step - accuracy: 0.9098 - loss: 0.2557 - val_accuracy: 0.9243 - val_loss: 0.1869 Epoch 5/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9212 - loss: 0.2227 - val_accuracy: 0.9339 - val_loss: 0.1684 Epoch 6/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 96ms/step - accuracy: 0.9256 - loss: 0.1998 - val_accuracy: 0.9291 - val_loss: 0.1833 Epoch 7/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 11s 103ms/step - accuracy: 0.9274 - loss: 0.1891 - val_accuracy: 0.9303 - val_loss: 0.1698 Epoch 8/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 97ms/step - accuracy: 0.9333 - loss: 0.1676 - val_accuracy: 0.9303 - val_loss: 0.1957 Epoch 9/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 92ms/step - accuracy: 0.9413 - loss: 0.1542 - val_accuracy: 0.9351 - val_loss: 0.1811 Epoch 10/20 105/105 ━━━━━━━━━━━━━━━━━━━━ 10s 93ms/step - accuracy: 0.9467 - loss: 0.1336 - val_accuracy: 0.8990 - val_loss: 0.2526
Found 240 files belonging to 2 classes.
Using 192 files for training.
Found 240 files belonging to 2 classes.
Using 48 files for validation.
Epoch 1/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 3s 212ms/step - accuracy: 0.5578 - loss: 0.6264 - val_accuracy: 0.8125 - val_loss: 0.4112 Epoch 2/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 103ms/step - accuracy: 0.8689 - loss: 0.2967 - val_accuracy: 0.8542 - val_loss: 0.3707 Epoch 3/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 100ms/step - accuracy: 0.9965 - loss: 0.0414 - val_accuracy: 0.9583 - val_loss: 0.0917 Epoch 4/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 97ms/step - accuracy: 0.9782 - loss: 0.0842 - val_accuracy: 1.0000 - val_loss: 0.0228 Epoch 5/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 101ms/step - accuracy: 0.9416 - loss: 0.1860 - val_accuracy: 1.0000 - val_loss: 0.0080 Epoch 6/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 98ms/step - accuracy: 0.9619 - loss: 0.0715 - val_accuracy: 1.0000 - val_loss: 0.0368 Epoch 7/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 99ms/step - accuracy: 0.9704 - loss: 0.0905 - val_accuracy: 0.9792 - val_loss: 0.1263 Epoch 8/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 100ms/step - accuracy: 0.9834 - loss: 0.0578 - val_accuracy: 0.9792 - val_loss: 0.0480 Epoch 9/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 96ms/step - accuracy: 1.0000 - loss: 0.0138 - val_accuracy: 1.0000 - val_loss: 0.0097 Epoch 10/20 6/6 ━━━━━━━━━━━━━━━━━━━━ 1s 98ms/step - accuracy: 0.9883 - loss: 0.0208 - val_accuracy: 1.0000 - val_loss: 0.0139
def extract_feature_predictions(image_dir, model_dict, img_size=(128, 128)):
data = []
labels = []
filenames = []
class_folders = ["Alex", "Kelly"]
label_map = {"Alex": 0, "Kelly": 1}
for class_name in class_folders:
class_path = os.path.join(image_dir, class_name)
for fname in os.listdir(class_path):
if fname.lower().endswith(('.jpg', '.jpeg', '.png')):
img_path = os.path.join(class_path, fname)
img = image.load_img(img_path, target_size=img_size)
img_array = image.img_to_array(img) / 255.0
img_array = np.expand_dims(img_array, axis=0)
row = []
for feature_name, model in model_dict.items():
pred = model.predict(img_array, verbose=0)[0][0] # Sigmoid output
row.append(pred)
data.append(row)
labels.append(label_map[class_name])
filenames.append(fname)
return np.array(data), np.array(labels), filenamesmodel_dict = {
"human": human_model,
"castle": castle_model,
"flower": flower_model,
"vehicle": vehicle_model,
"pet": pet_model,
"indoor_outdoor": indoor_outdoor_model,
"forest": forest_model,
"glacier": glacier_model,
"mountain": mountain_model,
"sea": sea_model,
"street": street_model,
"daynight": daynight_model,
}
X, y, filenames = extract_feature_predictions("Alex_Kelly_Pics/train", model_dict)WARNING:tensorflow:5 out of the last 24 calls to <function TensorFlowTrainer.make_predict_function.<locals>.one_step_on_data_distributed at 0x0000015BB8B4A020> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.
WARNING:tensorflow:6 out of the last 25 calls to <function TensorFlowTrainer.make_predict_function.<locals>.one_step_on_data_distributed at 0x0000015BB8B4A200> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_val)
print("Validation Accuracy:", accuracy_score(y_val, y_pred))
print(classification_report(y_val, y_pred, target_names=["Alex", "Kelly"]))Validation Accuracy: 0.7525773195876289
precision recall f1-score support
Alex 0.76 0.76 0.76 51
Kelly 0.74 0.74 0.74 46
accuracy 0.75 97
macro avg 0.75 0.75 0.75 97
weighted avg 0.75 0.75 0.75 97
def predict_test_images(test_dir, model_dict, final_classifier, threshold=0.6, img_size=(128, 128)):
results = []
for fname in sorted(os.listdir(test_dir)):
if fname.lower().endswith(('.jpg', '.jpeg', '.png')):
img_path = os.path.join(test_dir, fname)
img = image.load_img(img_path, target_size=img_size)
img_array = image.img_to_array(img) / 255.0
img_array = np.expand_dims(img_array, axis=0)
feature_row = []
for _, model in model_dict.items():
pred = model.predict(img_array, verbose=0)[0][0]
feature_row.append(pred)
feature_row = np.array(feature_row).reshape(1, -1)
proba = final_classifier.predict_proba(feature_row)[0]
predicted_idx = np.argmax(proba)
if max(proba) < threshold:
predicted_label = "Neither"
else:
predicted_label = "Alex" if predicted_idx == 0 else "Kelly"
results.append((fname, predicted_label, round(max(proba), 2)))
return resultsTestSetImage01.png: Alex (0.69)
TestSetImage02.png: Neither (0.56)
TestSetImage03.png: Kelly (0.77)
TestSetImage04.png: Kelly (0.73)
TestSetImage05.png: Alex (0.69)
TestSetImage06.png: Kelly (0.67)
TestSetImage07.png: Neither (0.55)
TestSetImage08.png: Kelly (0.81)
TestSetImage09.png: Kelly (0.67)
TestSetImage10.png: Kelly (0.76)
TestSetImage11.png: Alex (0.74)
TestSetImage12.png: Neither (0.51)
TestSetImage13.png: Kelly (0.81)
TestSetImage14.png: Kelly (0.71)
TestSetImage15.png: Alex (0.66)
TestSetImage16.png: Neither (0.51)
TestSetImage17.png: Neither (0.57)
TestSetImage18.png: Neither (0.53)
TestSetImage19.png: Alex (0.6)
TestSetImage20.png: Kelly (0.88)
from PIL import Image
# Folder where your test images are stored
test_image_dir = "Alex_Kelly_Pics/test/TestSet" # Adjust if needed
# Plotting
n_images = len(test_results)
n_cols = 5
n_rows = (n_images + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 3 * n_rows))
fig.suptitle("Test Image Predictions", fontsize=18)
for ax, (filename, pred, conf) in zip(axes.flatten(), test_results):
image_path = os.path.join(test_image_dir, filename)
image = Image.open(image_path)
ax.imshow(image)
ax.axis("off")
ax.set_title(f"{pred} ({conf:.2f})", fontsize=10)
# Hide any empty subplots
for i in range(len(test_results), n_rows * n_cols):
axes.flatten()[i].axis("off")
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()Train a convolutional neural network to directly classify the images as being taken by Alex or Kelly.
Work in a group of 3 students.
You are not allowed to use image metadata.
Your final deliverable must:
The training set contains only two photographers. Because your final predictions involve three classes, you may use external data to improve your models.
Your notebook should be narrated for a non-expert audience.
Include all code, but explain your steps in accessible, beginner-friendly language.
To prevent overfitting and ensure generalization, we used early stopping with restore_best_weights=True. This allowed the model to automatically revert to the epoch that achieved the lowest validation loss. As a result, our final model reflects the best performance observed during training.
We begin by setting a seed value. This is a technical step that ensures the results of our model are reproducible. Without setting this seed, we might get slightly different outcomes every time we run the model, which can make comparisons difficult. By fixing the seed for TensorFlow, NumPy, and Python’s random number generators, we create a consistent starting point for training.
Next, we load in our image data from folders using a TensorFlow utility. Each image is automatically labeled based on the folder it’s in — such as “Alex” or “Kelly.” We split this data into a training set and a validation set using an 80/20 ratio. Before training, we calculate class weights to handle any imbalance — for instance, if there are more photos of Kelly than Alex, the model might start favoring one class. We correct for that imbalance so it doesn’t skew the predictions.
Once the class weights are ready, we reload the images and apply preprocessing. First, we rescale the image pixel values to fall between 0 and 1 instead of the original 0 to 255. This helps the model train more smoothly. We also add image augmentation, which slightly changes the training images by flipping them, rotating them, or zooming in. These small random changes simulate real-world variety and help the model become more generalizable, rather than memorizing specific images.
For our actual model, we use MobileNetV2, a pre-trained CNN that has already been trained on millions of images. We load this model without its final classification layers so that we can add our own custom layers for our task. We also freeze the pre-trained layers so that their learned features are preserved — we don’t want to accidentally unlearn what the model already knows. On top of MobileNetV2, we add new layers: one that condenses the image data, another that helps prevent overfitting (called a dropout layer), and finally two layers that perform the classification — ending with a softmax layer that assigns a probability to each class (Alex, Kelly, or Neither).
The model is compiled using the Adam optimizer, which is widely used because it adapts the learning rate during training. We use sparse categorical crossentropy as the loss function, which is appropriate when you’re predicting one of several possible categories. We also monitor accuracy during training. To avoid training for too long and overfitting the data, we include an early stopping mechanism. This watches the validation performance and stops training if things stop improving.In the end, this function returns the trained model and a record of the training process, so we can later visualize how performance changed over time.
# Set seeds for reproducibility
seed_value = 42
tf.random.set_seed(seed_value)
np.random.seed(seed_value)
random.seed(seed_value)
os.environ['PYTHONHASHSEED'] = str(seed_value)
def train_alex_kelly_classifier_transfer(data_dir="Alex_Kelly_Pics/train", img_size=(128, 128), batch_size=32, epochs=30):
# Step 1: Load raw dataset for class weight calculation (use int labels)
raw_label_ds = tf.keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=SEED,
image_size=img_size,
batch_size=batch_size,
label_mode="int"
)
y_train_int = []
for _, labels in raw_label_ds:
y_train_int.extend(labels.numpy())
y_train_int = np.array(y_train_int)
class_weights = dict(enumerate(class_weight.compute_class_weight(
class_weight='balanced',
classes=np.unique(y_train_int),
y=y_train_int
)))
# Step 2: Load datasets again for model training (same int labels)
train_ds = tf.keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=42,
image_size=img_size,
batch_size=batch_size,
label_mode="int"
)
val_ds = tf.keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=42,
image_size=img_size,
batch_size=batch_size,
label_mode="int"
)
# Step 3: Normalize and augment
rescale = layers.Rescaling(1./255)
augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1),
layers.RandomZoom(0.1)
])
train_ds = train_ds.map(lambda x, y: (augmentation(rescale(x)), y)).cache().prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.map(lambda x, y: (rescale(x), y)).cache().prefetch(tf.data.AUTOTUNE)
# Step 4: Define transfer learning model
base_model = applications.MobileNetV2(input_shape=img_size + (3,),
include_top=False,
weights="imagenet")
base_model.trainable = False # Freeze base
model = models.Sequential([
base_model,
layers.GlobalAveragePooling2D(),
layers.Dropout(0.5),
layers.Dense(64, activation='relu', kernel_regularizer=regularizers.l2(0.01)),
layers.Dense(3, activation='softmax') # 3 classes: Alex, Kelly, Neither
])
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
# Step 5: Train with early stopping and class weights
early_stop = callbacks.EarlyStopping(patience=5, restore_best_weights=True)
history = model.fit(train_ds, validation_data=val_ds,
epochs=epochs,
class_weight=class_weights,
callbacks=[early_stop])
return model, historyFound 715 files belonging to 3 classes. Using 572 files for training. Found 715 files belonging to 3 classes. Using 572 files for training. Found 715 files belonging to 3 classes. Using 143 files for validation. Epoch 1/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 11s 332ms/step - accuracy: 0.3909 - loss: 2.8884 - val_accuracy: 0.5874 - val_loss: 2.0407 Epoch 2/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 4s 203ms/step - accuracy: 0.6085 - loss: 1.9375 - val_accuracy: 0.7483 - val_loss: 1.7938 Epoch 3/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 186ms/step - accuracy: 0.7181 - loss: 1.7411 - val_accuracy: 0.7552 - val_loss: 1.6054 Epoch 4/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 187ms/step - accuracy: 0.7780 - loss: 1.5105 - val_accuracy: 0.7622 - val_loss: 1.4773 Epoch 5/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 187ms/step - accuracy: 0.8057 - loss: 1.3489 - val_accuracy: 0.7483 - val_loss: 1.4210 Epoch 6/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 190ms/step - accuracy: 0.7876 - loss: 1.2734 - val_accuracy: 0.7552 - val_loss: 1.2917 Epoch 7/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 190ms/step - accuracy: 0.8228 - loss: 1.1805 - val_accuracy: 0.7692 - val_loss: 1.2506 Epoch 8/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 188ms/step - accuracy: 0.8481 - loss: 1.0960 - val_accuracy: 0.7552 - val_loss: 1.2214 Epoch 9/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 180ms/step - accuracy: 0.8374 - loss: 1.0201 - val_accuracy: 0.7483 - val_loss: 1.1981 Epoch 10/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 186ms/step - accuracy: 0.8673 - loss: 0.9255 - val_accuracy: 0.7483 - val_loss: 1.1529 Epoch 11/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 184ms/step - accuracy: 0.8868 - loss: 0.8596 - val_accuracy: 0.8042 - val_loss: 1.0512 Epoch 12/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 193ms/step - accuracy: 0.8597 - loss: 0.8630 - val_accuracy: 0.8182 - val_loss: 1.0145 Epoch 13/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 188ms/step - accuracy: 0.8894 - loss: 0.8048 - val_accuracy: 0.7972 - val_loss: 0.9863 Epoch 14/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 187ms/step - accuracy: 0.8619 - loss: 0.8169 - val_accuracy: 0.7692 - val_loss: 1.0070 Epoch 15/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 192ms/step - accuracy: 0.8930 - loss: 0.7495 - val_accuracy: 0.7972 - val_loss: 0.9375 Epoch 16/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 4s 195ms/step - accuracy: 0.8872 - loss: 0.7538 - val_accuracy: 0.8112 - val_loss: 0.9361 Epoch 17/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 4s 202ms/step - accuracy: 0.9021 - loss: 0.6993 - val_accuracy: 0.8042 - val_loss: 0.9236 Epoch 18/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 194ms/step - accuracy: 0.9002 - loss: 0.6786 - val_accuracy: 0.8042 - val_loss: 0.8851 Epoch 19/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 191ms/step - accuracy: 0.9087 - loss: 0.6401 - val_accuracy: 0.7902 - val_loss: 0.8603 Epoch 20/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 185ms/step - accuracy: 0.9140 - loss: 0.6145 - val_accuracy: 0.7972 - val_loss: 0.8797 Epoch 21/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 189ms/step - accuracy: 0.9293 - loss: 0.5581 - val_accuracy: 0.7832 - val_loss: 0.8514 Epoch 22/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 192ms/step - accuracy: 0.9165 - loss: 0.5673 - val_accuracy: 0.7972 - val_loss: 0.8549 Epoch 23/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 193ms/step - accuracy: 0.9141 - loss: 0.5467 - val_accuracy: 0.7762 - val_loss: 0.8611 Epoch 24/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 192ms/step - accuracy: 0.9158 - loss: 0.5545 - val_accuracy: 0.7832 - val_loss: 0.8672 Epoch 25/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 4s 195ms/step - accuracy: 0.9352 - loss: 0.4893 - val_accuracy: 0.7832 - val_loss: 0.8145 Epoch 26/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 193ms/step - accuracy: 0.9481 - loss: 0.4676 - val_accuracy: 0.7762 - val_loss: 0.8649 Epoch 27/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 188ms/step - accuracy: 0.9432 - loss: 0.4723 - val_accuracy: 0.7762 - val_loss: 0.8218 Epoch 28/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 192ms/step - accuracy: 0.9453 - loss: 0.4742 - val_accuracy: 0.7762 - val_loss: 0.7677 Epoch 29/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 191ms/step - accuracy: 0.9481 - loss: 0.4791 - val_accuracy: 0.8182 - val_loss: 0.7317 Epoch 30/30 18/18 ━━━━━━━━━━━━━━━━━━━━ 3s 179ms/step - accuracy: 0.9375 - loss: 0.4496 - val_accuracy: 0.8322 - val_loss: 0.7376
from tensorflow.keras.preprocessing import image
def predict_and_visualize_thresholded(model, test_dir, class_names, img_size=(128, 128), threshold=0.6, max_images=20):
image_files = sorted([
f for f in os.listdir(test_dir)
if f.lower().endswith(('.jpg', '.jpeg', '.png'))
])[:max_images]
plt.figure(figsize=(15, 8))
for i, fname in enumerate(image_files):
img_path = os.path.join(test_dir, fname)
img = image.load_img(img_path, target_size=img_size)
img_array = image.img_to_array(img) / 255.0
img_array = np.expand_dims(img_array, axis=0)
prediction = model.predict(img_array, verbose=0)[0]
predicted_idx = np.argmax(prediction)
confidence = prediction[predicted_idx]
if confidence < threshold:
predicted_label = "Neither"
else:
predicted_label = class_names[predicted_idx]
ax = plt.subplot(4, 5, i + 1)
plt.imshow(img)
plt.title(f"{predicted_label}\n({confidence:.2f})", fontsize=9)
plt.axis("off")
plt.tight_layout()
plt.show()